openai apps mcp

安装量: 302
排名: #3029

安装

npx skills add https://github.com/jezweb/claude-skills --skill 'OpenAI Apps MCP'
Building OpenAI Apps with Stateless MCP Servers
Status
Production Ready
Last Updated
2026-01-21
Dependencies
:
cloudflare-worker-base
,
hono-routing
(optional)
Latest Versions
@modelcontextprotocol/sdk@1.25.3, hono@4.11.3, zod@4.3.5, wrangler@4.58.0
Overview
Build
ChatGPT Apps
using
MCP (Model Context Protocol)
servers on Cloudflare Workers. Extends ChatGPT with custom tools and interactive widgets (HTML/JS UI rendered in iframe).
Architecture
ChatGPT → MCP endpoint (JSON-RPC 2.0) → Tool handlers → Widget resources (HTML)
Status
Apps available to Business/Enterprise/Edu (GA Nov 13, 2025). MCP Apps Extension (SEP-1865) formalized Nov 21, 2025.
Quick Start
1. Scaffold & Install
npm
create cloudflare@latest my-openai-app --
--type
hello-world
--ts
--git
--deploy
false
cd
my-openai-app
npm
install
@modelcontextprotocol/sdk@1.25.3 hono@4.11.3 zod@4.3.5
npm
install
-D
@cloudflare/vite-plugin@1.17.1 vite@7.2.4
2. Configure wrangler.jsonc
{
"name": "my-openai-app",
"main": "dist/index.js",
"compatibility_flags": ["nodejs_compat"], // Required for MCP SDK
"assets": {
"directory": "dist/client",
"binding": "ASSETS" // Must match TypeScript
}
}
3. Create MCP Server (
src/index.ts
)
import
{
Hono
}
from
'hono'
;
import
{
cors
}
from
'hono/cors'
;
import
{
Server
}
from
'@modelcontextprotocol/sdk/server/index.js'
;
import
{
ListToolsRequestSchema
,
CallToolRequestSchema
}
from
'@modelcontextprotocol/sdk/types.js'
;
const
app
=
new
Hono
<
{
Bindings
:
{
ASSETS
:
Fetcher
}
}
>
(
)
;
// CRITICAL: Must allow chatgpt.com
app
.
use
(
'/mcp/*'
,
cors
(
{
origin
:
'https://chatgpt.com'
}
)
)
;
const
mcpServer
=
new
Server
(
{
name
:
'my-app'
,
version
:
'1.0.0'
}
,
{
capabilities
:
{
tools
:
{
}
,
resources
:
{
}
}
}
)
;
// Tool registration
mcpServer
.
setRequestHandler
(
ListToolsRequestSchema
,
async
(
)
=>
(
{
tools
:
[
{
name
:
'hello'
,
description
:
'Use this when user wants to see a greeting'
,
inputSchema
:
{
type
:
'object'
,
properties
:
{
name
:
{
type
:
'string'
}
}
,
required
:
[
'name'
]
}
,
annotations
:
{
openai
:
{
outputTemplate
:
'ui://widget/hello.html'
}
// Widget URI
}
}
]
}
)
)
;
// Tool execution
mcpServer
.
setRequestHandler
(
CallToolRequestSchema
,
async
(
request
)
=>
{
if
(
request
.
params
.
name
===
'hello'
)
{
const
{
name
}
=
request
.
params
.
arguments
as
{
name
:
string
}
;
return
{
content
:
[
{
type
:
'text'
,
text
:
`
Hello,
${
name
}
!
`
}
]
,
_meta
:
{
initialData
:
{
name
}
}
// Passed to widget
}
;
}
throw
new
Error
(
`
Unknown tool:
${
request
.
params
.
name
}
`
)
;
}
)
;
app
.
post
(
'/mcp'
,
async
(
c
)
=>
{
const
body
=
await
c
.
req
.
json
(
)
;
const
response
=
await
mcpServer
.
handleRequest
(
body
)
;
return
c
.
json
(
response
)
;
}
)
;
app
.
get
(
'/widgets/*'
,
async
(
c
)
=>
c
.
env
.
ASSETS
.
fetch
(
c
.
req
.
raw
)
)
;
export
default
app
;
4. Create Widget (
src/widgets/hello.html
)
<!
DOCTYPE
html
>
<
html
>
<
head
>
<
style
>
body
{
margin
:
0
;
padding
:
20
px
;
font-family
:
system-ui
;
}
</
style
>
</
head
>
<
body
>
<
div
id
=
"
greeting
"
>
Loading...
</
div
>
<
script
>
if
(
window
.
openai
&&
window
.
openai
.
getInitialData
)
{
const
data
=
window
.
openai
.
getInitialData
(
)
;
document
.
getElementById
(
'greeting'
)
.
textContent
=
`
Hello,
${
data
.
name
}
! 👋
`
;
}
</
script
>
</
body
>
</
html
>
5. Deploy
npm
run build
npx wrangler deploy
npx @modelcontextprotocol/inspector https://my-app.workers.dev/mcp
Critical Requirements
CORS
Must allow
https://chatgpt.com
on
/mcp/*
routes
Widget URI
Must use
ui://widget/
prefix (e.g.,
ui://widget/map.html
)
MIME Type
Must be
text/html+skybridge
for HTML resources
Widget Data
Pass via
_meta.initialData
(accessed via
window.openai.getInitialData()
)
Tool Descriptions
Action-oriented ("Use this when user wants to...")
ASSETS Binding
Serve widgets from ASSETS, not bundled in worker code
SSE
Send heartbeat every 30s (100s timeout on Workers)
Known Issues Prevention
This skill prevents
14
documented issues:
Issue #1: CORS Policy Blocks MCP Endpoint
Error
:
Access to fetch blocked by CORS policy
Fix
:
app.use('/mcp/*', cors({ origin: 'https://chatgpt.com' }))
Issue #2: Widget Returns 404 Not Found
Error
:
404 (Not Found)
for widget URL
Fix
Use
ui://widget/
prefix (not
resource://
or
/widgets/
)
annotations
:
{
openai
:
{
outputTemplate
:
'ui://widget/map.html'
}
}
Issue #3: Widget Displays as Plain Text
Error
HTML source code visible instead of rendered widget
Fix
MIME type must be
text/html+skybridge
(not
text/html
)
server
.
setRequestHandler
(
ListResourcesRequestSchema
,
async
(
)
=>
(
{
resources
:
[
{
uri
:
'ui://widget/map.html'
,
mimeType
:
'text/html+skybridge'
}
]
}
)
)
;
Issue #4: ASSETS Binding Undefined
Error
:
TypeError: Cannot read property 'fetch' of undefined
Fix
Binding name in wrangler.jsonc must match TypeScript
{ "assets": { "binding": "ASSETS" } } // wrangler.jsonc
type
Bindings
=
{
ASSETS
:
Fetcher
}
;
// index.ts
Issue #5: SSE Connection Drops After 100 Seconds
Error
SSE stream closes unexpectedly
Fix
Send heartbeat every 30s (Workers timeout at 100s inactivity)
const
heartbeat
=
setInterval
(
async
(
)
=>
{
await
stream
.
writeSSE
(
{
data
:
JSON
.
stringify
(
{
type
:
'heartbeat'
}
)
,
event
:
'ping'
}
)
;
}
,
30000
)
;
Issue #6: ChatGPT Doesn't Suggest Tool
Error
Tool registered but never appears in suggestions
Fix
Use action-oriented descriptions
// ✅ Good: 'Use this when user wants to see a location on a map'
// ❌ Bad: 'Shows a map'
Issue #7: Widget Can't Access Initial Data
Error
:
window.openai.getInitialData()
returns
undefined
Fix
Pass data via
_meta.initialData
return
{
content
:
[
{
type
:
'text'
,
text
:
'Here is your map'
}
]
,
_meta
:
{
initialData
:
{
location
:
'SF'
,
zoom
:
12
}
}
}
;
Issue #8: Widget Scripts Blocked by CSP
Error
:
Refused to load script (CSP directive)
Fix
Use inline scripts or same-origin scripts. Third-party CDNs blocked.

< script

console . log ( 'ok' ) ; </ script

<
script
src
=
"
https://cdn.example.com/lib.js
"
>
</
script
>
Issue #9: Hono Global Response Override Breaks Next.js (v1.25.0-1.25.2)
Error
:
No response is returned from route handler
(Next.js App Router)
Source
:
GitHub Issue #1369
Affected Versions
v1.25.0 to v1.25.2
Fixed In
v1.25.3
Why It Happens
Hono (MCP SDK dependency) overwrites
global.Response
, breaking frameworks that extend it (Next.js, Remix, SvelteKit). NextResponse instanceof check fails.
Prevention
:
Upgrade to v1.25.3+
(recommended)
Before fix
Use
webStandardStreamableHTTPServerTransport
instead
Or
Run MCP server on separate port from Next.js/Remix/SvelteKit app
// ✅ v1.25.3+ - Fixed
const
transport
=
new
StreamableHTTPServerTransport
(
{
sessionIdGenerator
:
undefined
,
}
)
;
// ✅ v1.25.0-1.25.2 - Workaround
import
{
webStandardStreamableHTTPServerTransport
}
from
'@modelcontextprotocol/sdk/server/index.js'
;
const
transport
=
webStandardStreamableHTTPServerTransport
(
{
sessionIdGenerator
:
undefined
,
}
)
;
Issue #10: Elicitation (User Input) Fails on Cloudflare Workers
Error
:
EvalError: Code generation from strings disallowed
Source
:
GitHub Issue #689
Why It Happens
Internal AJV v6 validator uses prohibited APIs on edge platforms
Prevention
Avoid
elicitInput()
on edge platforms (Cloudflare Workers, Vercel Edge, Deno Deploy)
Workaround
:
// ❌ Don't use on Cloudflare Workers
const
userInput
=
await
server
.
elicitInput
(
{
prompt
:
"What is your name?"
,
schema
:
{
type
:
"string"
}
}
)
;
// ✅ Use tool parameters instead
server
.
setRequestHandler
(
CallToolRequestSchema
,
async
(
request
)
=>
{
const
{
name
}
=
request
.
params
.
arguments
as
{
name
:
string
}
;
// User provides via tool call, not elicitation
}
)
;
Status
Requires MCP SDK v2 to fix properly. Track
PR #844
.
Issue #11: SSE Transport Statefulness Breaks Serverless Deployments
Error
:
400: No transport found for sessionId
Source
:
GitHub Issue #273
Why It Happens
:
SSEServerTransport
relies on in-memory session storage. In serverless environments (AWS Lambda, Cloudflare Workers), the initial
GET /sse
request may be handled by Instance A, but subsequent
POST /messages
requests land on Instance B, which lacks the in-memory state.
Prevention
Use
Streamable HTTP transport
(added in v1.24.0) instead of SSE for serverless deployments
Solution
For stateful SSE, deploy to non-serverless environments (VPS, long-running containers)
Official Status
Fixed by introducing Streamable HTTP (v1.24+) - now the
recommended standard
for serverless.
Issue #12: OAuth Configuration Requires TWO Separate Apps
Source
:
Cloudflare Remote MCP Server Docs
Why It Happens
OAuth providers validate redirect URLs strictly. Localhost and production have different URLs, so they need separate OAuth client registrations. Prevention :

Development OAuth App

Callback URL: http://localhost:8788/callback

Production OAuth App

Callback URL: https://my-mcp-server.workers.dev/callback
Additional Requirements
:
KV namespace for auth state storage (create manually)
COOKIE_ENCRYPTION_KEY
env var:
openssl rand -hex 32
Client restart required after config changes
Issue #13: Widget State Over 4k Tokens Causes Performance Issues (Community-sourced)
Source
:
OpenAI Apps SDK - ChatGPT UI
Why It Happens
Widget state persists only to a single widget instance tied to one conversation message. State is reset when users submit via the main chat composer instead of widget controls.
Prevention
Keep state payloads under
4k tokens
for optimal performance
// ✅ Good - Lightweight state
window
.
openai
.
setWidgetState
(
{
selectedId
:
"item-123"
,
view
:
"grid"
}
)
;
// ❌ Bad - Will cause performance issues
window
.
openai
.
setWidgetState
(
{
items
:
largeArray
,
// Don't store full datasets
history
:
conversationLog
,
// Don't store conversation history
cache
:
expensiveComputation
// Don't cache large results
}
)
;
Best Practice
:
Store only UI state (selected items, view mode, filters)
Fetch data from MCP server on widget mount
Use tool calls to persist important data
Issue #14: Widget-Initiated Tool Calls Fail Without Permission Flag (Community-sourced)
Source
:
OpenAI Apps SDK - ChatGPT UI
Why It Happens
Components initiating tool calls via
window.openai.callTool()
require the tool marked as "able to be initiated by the component" on the MCP server. Without this flag, calls fail silently.
Prevention
Mark tools as
widgetCallable: true
in annotations
// MCP Server - Mark tool as widget-callable
server
.
setRequestHandler
(
ListToolsRequestSchema
,
async
(
)
=>
(
{
tools
:
[
{
name
:
'update_item'
,
description
:
'Update an item'
,
inputSchema
:
{
/ ... /
}
,
annotations
:
{
openai
:
{
outputTemplate
:
'ui://widget/item.html'
,
// ✅ Required for widget-initiated calls
widgetCallable
:
true
}
}
}
]
}
)
)
;
// Widget - Now allowed to call tool
window
.
openai
.
callTool
(
{
name
:
'update_item'
,
arguments
:
{
id
:
itemId
,
status
:
'completed'
}
}
)
;
Widget Development Best Practices
File Upload Limitations (Community-sourced)
Source
:
OpenAI Apps SDK - ChatGPT UI
window.openai.uploadFile()
only supports 3 image formats:
image/png
,
image/jpeg
, and
image/webp
. Other formats fail silently.
// ✅ Supported
window
.
openai
.
uploadFile
(
{
accept
:
'image/png,image/jpeg,image/webp'
}
)
;
// ❌ Not supported (fails silently)
window
.
openai
.
uploadFile
(
{
accept
:
'application/pdf'
}
)
;
window
.
openai
.
uploadFile
(
{
accept
:
'text/csv'
}
)
;
Alternative for Other File Types
:
Use base64 encoding in tool arguments
Request user paste text content
Use external upload service (S3, R2) and pass URL
Tool Performance Targets (Community-sourced)
Source
:
OpenAI Apps SDK - Troubleshooting
Tool calls exceeding "a few hundred milliseconds" cause UI sluggishness in ChatGPT. Official docs recommend profiling backends and implementing caching for slow operations.
Performance Targets
:
< 200ms
Ideal response time
200-500ms
Acceptable but noticeable
> 500ms
Sluggish, needs optimization
Optimization Strategies
:
// 1. Cache expensive computations
const
cache
=
new
Map
(
)
;
if
(
cache
.
has
(
key
)
)
return
cache
.
get
(
key
)
;
const
result
=
await
expensiveOperation
(
)
;
cache
.
set
(
key
,
result
)
;
// 2. Use KV/D1 for pre-computed data
const
cached
=
await
env
.
KV
.
get
(
`
result:
${
id
}
`
)
;
if
(
cached
)
return
JSON
.
parse
(
cached
)
;
// 3. Paginate large datasets
return
{
content
:
[
{
type
:
'text'
,
text
:
'First 20 results...'
}
]
,
_meta
:
{
hasMore
:
true
,
nextPage
:
2
}
}
;
// 4. Move slow work to async tasks
// Return immediately, update via follow-up
MCP SDK 1.25.x Updates (December 2025)
Breaking Changes
from @modelcontextprotocol/sdk@1.24.x → 1.25.x:
Removed loose type exports (Prompts, Resources, Roots, Sampling, Tools) - use specific schemas
ES2020 target required (previous: ES2018)
setRequestHandler
is now typesafe - incorrect schemas throw type errors
New Features
:
Tasks
(v1.24.0+): Long-running operations with progress tracking
Sampling with Tools
(v1.24.0+): Tools can request model sampling
OAuth Client Credentials
(M2M): Machine-to-machine authentication
Migration
If using loose type imports, update to specific schema imports:
// ❌ Old (removed in 1.25.0)
import
{
Tools
}
from
'@modelcontextprotocol/sdk/types.js'
;
// ✅ New (1.25.1+)
import
{
ListToolsRequestSchema
,
CallToolRequestSchema
}
from
'@modelcontextprotocol/sdk/types.js'
;
Zod 4.0 Migration Notes (MAJOR UPDATE - July 2025)
Breaking Changes
from
zod@3.x
→ 4.x:
.default()
now expects input type (not output type). Use
.prefault()
for old behavior.
ZodError:
error.issues
(not
error.errors
)
.merge()
and
.superRefine()
deprecated
Optional properties with defaults now always apply
Performance
14x faster string parsing, 7x faster arrays, 6.5x faster objects
Migration
Update validation code:
// Zod 4.x
try
{
const
validated
=
schema
.
parse
(
data
)
;
}
catch
(
error
)
{
if
(
error
instanceof
z
.
ZodError
)
{
return
{
content
:
[
{
type
:
'text'
,
text
:
error
.
issues
.
map
(
e
=>
e
.
message
)
.
join
(
', '
)
}
]
}
;
}
}
Dependencies
{
"dependencies"
:
{
"@modelcontextprotocol/sdk"
:
"^1.25.3"
,
"hono"
:
"^4.11.3"
,
"zod"
:
"^4.3.5"
}
,
"devDependencies"
:
{
"@cloudflare/vite-plugin"
:
"^1.17.1"
,
"@cloudflare/workers-types"
:
"^4.20260103.0"
,
"vite"
:
"^7.2.4"
,
"wrangler"
:
"^4.54.0"
}
}
Official Documentation
MCP Specification
:
https://modelcontextprotocol.io/
(Latest: 2025-11-25)
MCP SDK
:
https://github.com/modelcontextprotocol/typescript-sdk
OpenAI Apps SDK
:
https://developers.openai.com/apps-sdk
MCP Apps Extension (SEP-1865)
:
http://blog.modelcontextprotocol.io/posts/2025-11-21-mcp-apps/
Context7 Library ID
/modelcontextprotocol/typescript-sdk
Production Reference
Open Source Example
:
https://github.com/jezweb/chatgpt-app-sdk
(portfolio carousel widget)
Live in Production
Rendering in ChatGPT Business
MCP Server
Full JSON-RPC 2.0 implementation with tools + resources (~310 lines)
Widget Integration
WordPress API →
window.openai.toolOutput
→ React carousel
Database
D1 (SQLite) for contact form submissions
Stack
Hono 4 + React 19 + Tailwind v4 + Drizzle ORM
Key Files
:
/src/lib/mcp/server.ts
- Complete MCP handler
/src/server/tools/portfolio.ts
- Tool with widget annotations
/src/widgets/PortfolioWidget.tsx
- Data access pattern
Verified
All 14 known issues prevented, zero errors in production
Community Resources
Deployment Tools
Cloudflare One-Click Deploy
Deploy MCP servers to Cloudflare Workers with pre-built templates and auto-configured CI/CD. Includes OAuth wrapper and Python support.
Docs:
https://developers.cloudflare.com/agents/guides/remote-mcp-server/
Blog:
https://blog.cloudflare.com/model-context-protocol/
Frameworks
Skybridge
(Community): React-focused framework with HMR support for widgets and enhanced MCP server helpers. Unofficial but actively maintained.
GitHub:
https://github.com/alpic-ai/skybridge
Docs:
https://www.skybridge.tech/
Note
Community frameworks are not officially supported. Use at your own discretion
返回排行榜